/* *****************************************************************************
 * JFab (http://code.google.com/p/jfab)
 * Copyright (c) 2011 JFab.org
 * Admin jfab@jeffreythompson.net
 *
 * See the file "LICENSE.txt" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 * *****************************************************************************/

package org.jfab.core.factory;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.commons.beanutils.converters.ClassConverter;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides an analyzer of class constructor, accessor and mutator methods.
 */
public final class ClassAnalyzer
{
    /** Logger. */
    private static final Logger LOGGER = LoggerFactory
            .getLogger(ClassAnalyzer.class);

    /** Cache of analyzers. */
    private static Map<Class<?>, ClassAnalyzer> _analyzers = new LinkedHashMap<Class<?>, ClassAnalyzer>();

    /**
     * Clear the cached analyzers.
     */
    public static final void clear()
    {
        _analyzers.clear();
    }

    /**
     * @param aClass A class.
     * 
     * @return a class analyzer instance. Clients must use this method to obtain
     *         analyzer objects which are cached as they are created.
     */
    public static final ClassAnalyzer getInstance(final Class<?> aClass)
    {
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("ClassAnalyzer", "getClassAnalyzer", "aClass = "
                    + aClass);
        }

        // Check the cache.
        ClassAnalyzer analyzer = _analyzers.get(aClass);

        if (analyzer == null)
        {
            // Create a new one, and cache it.
            analyzer = new ClassAnalyzer(aClass);
            _analyzers.put(aClass, analyzer);
        }

        return analyzer;
    }

    /**
     * @param className Class name.
     * 
     * @return a class analyzer instance. Clients must use this method to obtain
     *         analyzer objects which are cached as they are created.
     */
    public static final ClassAnalyzer getInstance(final String className)
    {
        final ClassConverter converter = new ClassConverter();
        final Class<?> aClass = (Class<?>)converter.convert(Class.class,
                className);

        return getInstance(aClass);
    }

    /**
     * @param propertyName Property name.
     * @param object Object.
     * 
     * @return the value of the given property from the given object. The
     *         property name may be chained, e.g. engine.environment.
     * 
     * @throws IllegalAccessException if there is an illegal access.
     * @throws InvocationTargetException if there is an invocation problem.
     */
    public static final Object getValue(final Object object,
            final String propertyName) throws IllegalAccessException,
            InvocationTargetException
    {
        Validate.notEmpty(propertyName, "propertyName is empty");
        Validate.notNull(object, "object is null");

        final ClassAnalyzer classAnalyzer = ClassAnalyzer.getInstance(object
                .getClass());

        Object answer = null;
        final String key = ".";
        final int index = propertyName.indexOf(key);

        if (index >= 0)
        {
            final String parentProperty = propertyName.substring(0, index);
            final String childProperty = propertyName.substring(index
                    + key.length());
            final Method accessor = classAnalyzer
                    .getAccessorMethodFor(parentProperty);
            final Object child = accessor.invoke(object);

            if ((child != null) && (childProperty != null))
            {
                answer = ClassAnalyzer.getValue(child, childProperty);
            }
        }
        else
        {
            if (LOGGER.isDebugEnabled())
            {
                LOGGER.debug("getting accessor for property " + propertyName);
            }

            final Method accessor = classAnalyzer
                    .getAccessorMethodFor(propertyName);

            if (accessor != null)
            {
                answer = accessor.invoke(object);
            }
            else
            {
                if (LOGGER.isDebugEnabled())
                {
                    LOGGER.debug("Couldn't locate accessor for property "
                            + propertyName);
                }
            }
        }

        return answer;
    }

    /**
     * Set the given value of the given property on the given object. The
     * property name may be chained, e.g. engine.environment.
     * 
     * @param object Object.
     * @param propertyName Property name.
     * @param newValue New value.
     * 
     * @throws IllegalAccessException if there is an illegal access.
     * @throws InvocationTargetException if there is an invocation problem.
     */
    public static final void setValue(final Object object,
            final String propertyName, final Object newValue)
            throws IllegalAccessException, InvocationTargetException
    {
        Validate.notEmpty(propertyName, "propertyName is empty");
        Validate.notNull(object, "object is null");

        final ClassAnalyzer classAnalyzer = ClassAnalyzer.getInstance(object
                .getClass());

        final String key = ".";
        final int index = propertyName.indexOf(key);

        if (index >= 0)
        {
            final String parentProperty = propertyName.substring(0, index);
            final String childProperty = propertyName.substring(index
                    + key.length());
            final Method accessor = classAnalyzer
                    .getAccessorMethodFor(parentProperty);
            final Object child = accessor.invoke(object);

            if ((child != null) && (childProperty != null))
            {
                final Method mutator = classAnalyzer
                        .getMutatorMethodFor(childProperty);

                if (mutator != null)
                {
                    mutator.invoke(child, new Object[] { newValue });
                }
                else
                {
                    if (LOGGER.isDebugEnabled())
                    {
                        LOGGER.debug("Couldn't locate mutator for property "
                                + childProperty);
                    }
                }
            }
        }
        else
        {
            if (LOGGER.isDebugEnabled())
            {
                LOGGER.debug("getting accessor for property " + propertyName);
            }

            final Method mutator = classAnalyzer
                    .getMutatorMethodFor(propertyName);

            if (mutator != null)
            {
                mutator.invoke(object, new Object[] { newValue });
            }
            else
            {
                if (LOGGER.isDebugEnabled())
                {
                    LOGGER.debug("Couldn't locate mutator for property "
                            + propertyName);
                }
            }
        }
    }

    /** Map of accessor method name to accessor method. */
    private final Map<String, Method> _accessorMethods = new TreeMap<String, Method>();

    /** The bean info for the class. */
    private BeanInfo _beanInfo;

    /** The class analyzed. */
    private Class<?> _class;

    /** No-arg constructor, if any. */
    private Constructor<?> _constructor;

    /** Map of mutator method name to mutator method. */
    private final Map<String, Method> _mutatorMethods = new TreeMap<String, Method>();

    /** Map of property name to property descriptor. */
    private final Map<String, PropertyDescriptor> _propertyDescriptors = new TreeMap<String, PropertyDescriptor>();

    /**
     * A private constructor prevents non-cached instances.
     * 
     * @param aClass A class.
     */
    private ClassAnalyzer(final Class<?> aClass)
    {
        analyzeClass(aClass);
    }

    /**
     * @return the property names of the accessor methods.
     */
    public final Set<String> getAccessiblePropertyNames()
    {
        return _accessorMethods.keySet();
    }

    /**
     * @param propertyName Property name.
     * 
     * @return the accessor method for the given property name.
     */
    public final Method getAccessorMethodFor(final String propertyName)
    {
        return _accessorMethods.get(propertyName);
    }

    /**
     * @return the accessor methods.
     */
    public final Collection<Method> getAccessorMethods()
    {
        return _accessorMethods.values();
    }

    /**
     * @return the property names of the mutator methods.
     */
    public final Set<String> getMutablePropertyNames()
    {
        return _mutatorMethods.keySet();
    }

    /**
     * @param propertyName Property name.
     * 
     * @return the mutator method for the given property name.
     */
    public final Method getMutatorMethodFor(final String propertyName)
    {
        return _mutatorMethods.get(propertyName);
    }

    /**
     * @return the mutator methods.
     */
    public final Collection<Method> getMutatorMethods()
    {
        return _mutatorMethods.values();
    }

    /**
     * @return the no-arg constructor.
     */
    public final Constructor<?> getNoArgConstructor()
    {
        return _constructor;
    }

    /**
     * @param propertyName Property name.
     * 
     * @return the type of the given property.
     */
    public final Class<?> getPropertyType(final String propertyName)
    {
        Class<?> answer = null;
        final PropertyDescriptor propertyDescriptor = _propertyDescriptors
                .get(propertyName);

        if (propertyDescriptor != null)
        {
            answer = propertyDescriptor.getPropertyType();
        }

        return answer;
    }

    /**
     * @return the subject class.
     */
    public final Class<?> getSubjectClass()
    {
        return _class;
    }

    /**
     * Obtain the names of the properties, accessors, and mutators of the given
     * object's class.
     * 
     * @param aClass A class.
     */
    private void analyzeClass(final Class<?> aClass)
    {
        _class = aClass;

        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug(getClass().getName(), "analyzeClass", "aClass = "
                    + aClass);
        }

        try
        {
            _beanInfo = Introspector.getBeanInfo(_class);
        }
        catch (final IntrospectionException e)
        {
            e.printStackTrace();
        }

        // Get the no-arg constructor for the class.
        try
        {
            _constructor = aClass.getConstructor(new Class[] {});
        }
        catch (final NoSuchMethodException e)
        {
            // Fail silently.
        }

        final PropertyDescriptor[] propertyDescriptors = _beanInfo
                .getPropertyDescriptors();

        for (int i = 0; i < propertyDescriptors.length; i++)
        {
            final String propertyName = propertyDescriptors[i].getName();
            _propertyDescriptors.put(propertyName, propertyDescriptors[i]);

            final Method accessor = propertyDescriptors[i].getReadMethod();

            if (accessor != null)
            {
                if (LOGGER.isDebugEnabled())
                {
                    LOGGER.debug("adding accessor for property name :"
                            + propertyName + ":");
                }

                processMethod(_accessorMethods, propertyName, accessor);
            }

            final Method mutator = propertyDescriptors[i].getWriteMethod();

            if (mutator != null)
            {
                if (LOGGER.isDebugEnabled())
                {
                    LOGGER.debug("adding mutator for property name :"
                            + propertyName + ":");
                }

                processMethod(_mutatorMethods, propertyName, mutator);
            }
        }
    }

    /**
     * Process the given method and add it to the given collection.
     * 
     * @param map Map of property name to method.
     * @param propertyName Property name.
     * @param method Method.
     */
    private void processMethod(final Map<String, Method> map,
            final String propertyName, final Method method)
    {
        // Find the property name which goes with this method.
        boolean saveIt = true;

        // First, check for an existing method for this property name.
        final Method method2 = map.get(propertyName);

        if (method2 != null)
        {
            // Prefer the method which takes a String.
            final Class<?>[] parameterTypes = method2.getParameterTypes();

            if (parameterTypes[0] == String.class)
            {
                saveIt = false;
            }
        }

        if (saveIt)
        {
            // Save this method.
            map.put(propertyName, method);
        }
    }
}
